package com.hannesdorfmann.mosby3;

import com.hannesdorfmann.mosby3.mvp.MvpPresenter;
import com.hannesdorfmann.mosby3.mvp.MvpView;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.AbilityLifecycleCallbacks;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.ability.fraction.Fraction;
import ohos.app.Context;
import ohos.app.AbilityContext;
import ohos.app.ElementsCallback;
import ohos.global.configuration.Configuration;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.utils.LightweightMap;
import ohos.utils.PacMap;

import java.util.Map;
import java.util.UUID;

/**
 * A internal class responsible to save internal presenter instances during screen orientation
 * changes and reattach the presenter afterwards.
 *
 * <p>
 * The idea is that each MVP View (like a Activity, Fragment, ViewGroup) will get a unique view id.
 * This view id is
 * used to store the presenter and viewstate in it. After screen orientation changes we can reuse
 * the presenter and viewstate by querying for the given view id (must be saved in view's state
 * somehow).
 * </p>
 *
 * @author Hannes Dorfmann
 */
final public class PresenterManager {

    public final static boolean DEBUG = false;
    private static final String TAG = "PresenterManager";
    private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0, TAG);
    final static String KEY_ACTIVITY_ID = "com.hannesdorfmann.mosby3.MosbyPresenterManagerActivityId";

    private final static Map<Ability, String> activityIdMap = new LightweightMap();
    private final static Map<String, ActivityScopedCache> activityScopedCacheMap = new LightweightMap<>();

    static final ElementsCallback elementsCallback = new ElementsCallback() {
        @Override
        public void onMemoryLevel(int i) {

        }

        @Override
        public void onConfigurationUpdated(Configuration configuration) {

        }
    };
    static Ability activity;
    static final AbilityLifecycleCallbacks activityLifecycleCallbacks =
            new AbilityLifecycleCallbacks() {
                @Override
                public void onAbilityStart(Ability ability) {
                    activity = ability;
                }

                @Override
                public void onAbilityActive(Ability ability) {

                }

                @Override
                public void onAbilityInactive(Ability ability) {

                }

                @Override
                public void onAbilityForeground(Ability ability) {

                }

                @Override
                public void onAbilityBackground(Ability ability) {

                }

                @Override
                public void onAbilityStop(Ability activity) {
                    if (!activity.isUpdatingConfigurations()) {
                        // Activity will be destroyed permanently, so reset the cache
                        String activityId = activityIdMap.get(activity);
                        if (activityId != null) {
                            ActivityScopedCache scopedCache = activityScopedCacheMap.get(activityId);
                            if (scopedCache != null) {
                                scopedCache.clear();
                                activityScopedCacheMap.remove(activityId);
                            }

                            // No Activity Scoped cache available, so unregister
                            if (activityScopedCacheMap.isEmpty()) {
                                // All Mosby related abilities are destroyed, so we can remove the activity lifecylce listener
                                activity.getAbilityPackage().unregisterCallbacks(activityLifecycleCallbacks, elementsCallback);
                                if (DEBUG) {
                                  HiLog.debug(LABEL, "Unregistering AbilityLifecycleCallbacks");
                                }
                            }
                        }
                    }
                    activityIdMap.remove(activity);
                }

                @Override
                public void onAbilitySaveState(PacMap pacMap) {
                    String activityId = activityIdMap.get(activity);
                    if (activityId != null) {
                        pacMap.putString(KEY_ACTIVITY_ID, activityId);
                    }

                }
            };


    private PresenterManager() {
        throw new RuntimeException("Not instantiatable!");
    }

    /**
     * Get an already existing {@link ActivityScopedCache} or creates a new one if not existing yet
     *
     * @param activity The Activitiy for which you want to get the activity scope for
     * @return The {@link ActivityScopedCache} for the given Activity
     */
    static ActivityScopedCache getOrCreateActivityScopedCache(
            Ability activity) {
        if (activity == null) {
            throw new NullPointerException("Activity is null");
        }

        String activityId = activityIdMap.get(activity);
        if (activityId == null) {
            // Ability not registered yet
            activityId = UUID.randomUUID().toString();
            activityIdMap.put(activity, activityId);

            if (activityIdMap.size() == 1) {
                activity.getAbilityPackage().registerCallbacks(activityLifecycleCallbacks, elementsCallback);
            }
        }

        ActivityScopedCache activityScopedCache = activityScopedCacheMap.get(activityId);
        if (activityScopedCache == null) {
            activityScopedCache = new ActivityScopedCache();
            activityScopedCacheMap.put(activityId, activityScopedCache);
        }

        return activityScopedCache;
    }

    /**
     * Get the  {@link ActivityScopedCache} for the given Activity or <code>null</code> if no {@link
     * ActivityScopedCache} exists for the given Activity
     *
     * @param activity The activity
     * @return The {@link ActivityScopedCache} or null
     * @see #getOrCreateActivityScopedCache(Activity)
     */
    static ActivityScopedCache getActivityScope(Ability activity) {
        if (activity == null) {
            throw new NullPointerException("Activity is null");
        }
        String activityId = activityIdMap.get(activity);
        if (activityId == null) {
            return null;
        }

        return activityScopedCacheMap.get(activityId);
    }

    /**
     * Get the presenter for the View with the given (Mosby - internal) view Id or <code>null</code>
     * if no presenter for the given view (via view id) exists.
     *
     * @param activity The Activity (used for scoping)
     * @param viewId   The mosby internal View Id (unique among all {@link MvpView}
     * @param <P>      The Presenter type
     * @return The Presenter or <code>null</code>
     */
    public static <P> P getPresenter(Ability activity, String viewId) {
        if (activity == null) {
            throw new NullPointerException("Activity is null");
        }

        if (viewId == null) {
            throw new NullPointerException("View id is null");
        }

        ActivityScopedCache scopedCache = getActivityScope(activity);
        return scopedCache == null ? null : (P) scopedCache.getPresenter(viewId);
    }

    /**
     * Get the ViewState (see mosby viestate modlue) for the View with the given (Mosby - internal)
     * view Id or <code>null</code>
     * if no viewstate for the given view exists.
     *
     * @param activity The Activity (used for scoping)
     * @param viewId   The mosby internal View Id (unique among all {@link MvpView}
     * @param <VS>     The type of the ViewState type
     * @return The Presenter or <code>null</code>
     */
    public static <VS> VS getViewState(Ability activity, String viewId) {
        if (activity == null) {
            throw new NullPointerException("Activity is null");
        }

        if (viewId == null) {
            throw new NullPointerException("View id is null");
        }

        ActivityScopedCache scopedCache = getActivityScope(activity);
        return scopedCache == null ? null : (VS) scopedCache.getViewState(viewId);
    }

    /**
     * Get the Activity of a context. This is typically used to determine the hosting activity of a
     * {@link View}
     *
     * @param context The context
     * @return The Activity or throws an Exception if Activity couldnt be determined
     */
    public static Ability getActivity(Context context) {
        if (context == null) {
            throw new NullPointerException("context == null");
        }

        if (context instanceof AbilitySlice) {
            return ((AbilitySlice) context).getAbility();
        }

        if (context instanceof Ability) {
            return (Ability) context;
        }

        if (context instanceof Fraction) {
            return ((Fraction) context).getFractionAbility();
        }


        while (context instanceof AbilityContext) {
            if (context instanceof AbilitySlice) {
                return ((AbilitySlice) context).getAbility();
            }

            if (context instanceof Ability) {
                return (Ability) context;
            }

            if (context instanceof Fraction) {
                return ((Fraction) context).getFractionAbility();
            }
            context = ((AbilityContext) context).getApplicationContext();
        }
        throw new IllegalStateException("Could not find the surrounding Activity");
    }

    /**
     * Clears the internal (static) state. Used for testing.
     */
    static void reset() {
        activityIdMap.clear();
        for (ActivityScopedCache scopedCache : activityScopedCacheMap.values()) {
            scopedCache.clear();
        }

        activityScopedCacheMap.clear();
    }

    /**
     * Puts the presenter into the internal cache
     *
     * @param activity  The parent activity
     * @param viewId    the view id (mosby internal)
     * @param presenter the presenter
     */
    public static void putPresenter(Ability activity, String viewId,
                                    MvpPresenter<? extends MvpView> presenter) {
        if (activity == null) {
            throw new NullPointerException("Ability is null");
        }

        ActivityScopedCache scopedCache = getOrCreateActivityScopedCache(activity);
        scopedCache.putPresenter(viewId, presenter);
    }

    /**
     * Puts the presenter into the internal cache
     *
     * @param activity  The parent activity
     * @param viewId    the view id (mosby internal)
     * @param viewState the presenter
     */
    public static void putViewState(Ability activity, String viewId,
                                    Object viewState) {

        if (activity == null) {
            throw new NullPointerException("Ability is null");
        }

        ActivityScopedCache scopedCache = getOrCreateActivityScopedCache(activity);
        scopedCache.putViewState(viewId, viewState);
    }

    /**
     * Removes the Presenter (and ViewState) for the given View. Does nothing if no Presenter is
     * stored internally with the given viewId
     *
     */
    public static void remove(Ability activity, String viewId) {
        if (activity == null) {
            throw new NullPointerException("Ability is null");
        }

        ActivityScopedCache activityScope = getActivityScope(activity);
        if (activityScope != null) {
            activityScope.remove(viewId);
        }
    }
}



